// SPDX-License-Identifier: GPL-2.0

#include <linux/mm.h>
#include <linux/acpi.h>
#include <linux/vpmem.h>
#include <asm/setup.h>

#ifdef CONFIG_ARM64
#include <asm/vpmem/api.h>
#else
#include <asm/e820/api.h>
#endif

static struct vpmem_early_table e820_table_vpmem_init     __initdata;
struct vpmem_early_table *e820_table_vpmem __refdata      = &e820_table_vpmem_init;
static unsigned long vpmem_os_mem_size;
static bool vpmem_os_mem_auto;

static int __init e820__vpmem_memparse_check(u64 from, u64 to)
{
	if (!IS_ALIGNED(from, PMD_SIZE)) {
		pr_warn("VPMEM start address %#018Lx not aligned with 2M\n", from);
		return -EINVAL;
	}

	if (!IS_ALIGNED(to, PMD_SIZE)) {
		pr_warn("VPMEM end address %#018Lx not aligned with 2M\n", to);
		return -EINVAL;
	}
	if (from >= to) {
		pr_warn("Invalid VPMEM range [mem %#018Lx-%#018Lx]\n", from, to);
		return -EINVAL;
	}
	return 0;
}

static int __init parse_vpmemopt(char *p)
{
	int res;

	vpmem_set_userdef(1);
	res = vpmem_memparse(p, &vpmem_os_mem_auto, &vpmem_os_mem_size,
			     e820_table_vpmem, e820__vpmem_memparse_check);
	if (!res)
		vpmem_control_set();
	return res;
}
early_param("vpmem", parse_vpmemopt);

int __init e820__vpmem_init_table(void)
{
	struct acpi_table_header *header = NULL;
	acpi_status status;

	/* VPMEM table already initialized */
	if (e820_table_vpmem->nr_entries > 0)
		return 0;

	if (acpi_disabled) {
		pr_info("vpmem: acpi disabled, disable vpmem numa\n");
		return -ENODEV;
	}

	if (numa_off) {
		pr_info("vpmem: numa disabled, disable vpmem numa\n");
		return -ENODEV;
	}

	status = acpi_get_table(ACPI_SIG_SRAT, 0, &header);
	if (status == AE_NOT_FOUND || !header) {
		pr_info("vpmem: SRAT not found, disable vpmem numa\n");
		return -ENODEV;
	}

	if (vpmem_init_table(header, e820_table_vpmem) < 0)
		pr_warn("vpmem: too many vpmem entries, some maybe ignored\n");
	acpi_put_table(header);
	return 0;
}

static int __init e820__vpmem_signle_numa_exclusive(void)
{
	u64 entry_size;
	u64 size = vpmem_os_mem_size;
	u64 start = 0;

	/* ACPI disabled or NUMA disabled */
	entry_size = e820__mapped_find(&start, E820_TYPE_RAM);
	while (entry_size > 0) {
		if (entry_size >= size) {
			start += size;
			size = 0;
			break;
		}
		start += entry_size;
		size -= entry_size;
		entry_size = e820__mapped_find(&start, E820_TYPE_RAM);
	}

	if (start > 0 && size == 0) {
		start = ALIGN(start, PMD_SIZE);
		e820__range_update(start, ULLONG_MAX - start,
			E820_TYPE_RAM, E820_TYPE_VPMEM);
	}

	vpmem_update_memory_range();
	return 0;
}

static int __init e820__vpmem_mult_numa_equalize(void)
{
	int ret = vpmem_mult_numa_equalize(e820_table_vpmem, vpmem_os_mem_size);

	if (ret)
		pr_info("vpmem: mult-numa not found in machine\n");
	return ret;
}

static int __init e820__vpmem_update_table(void)
{
	int i;

	for (i = 0; i < e820_table_vpmem->nr_entries; i++) {
		struct vpmem_early_entry *entry = &e820_table_vpmem->entries[i];

		if (!entry->size)
			continue;
		e820__range_update(entry->addr, entry->size,
			E820_TYPE_RAM, E820_TYPE_VPMEM);
		vpmem_add_memory_range(entry->addr >> PAGE_SHIFT,
			(entry->addr + entry->size - 1) >> PAGE_SHIFT);
	}
	return 0;
}

static int __init e820__auto_set_vpmem_mem(void)
{
	int ret;

	if (!vpmem_os_mem_auto)
		return 0;
	ret = e820__vpmem_init_table();
	if (ret < 0)
		return ret;

	vpmem_os_mem_size = vpmem_get_auto_os_mem_size(e820_table_vpmem);
	return 0;
}

int __init e820__mark_vpmem(void)
{
	int ret = e820__auto_set_vpmem_mem();

	if (ret < 0)
		return ret;

	if (!vpmem_os_mem_size)
		return e820__vpmem_update_table();

	/* ACPI disabled or NUMA disabled */
	if (e820__vpmem_init_table() < 0)
		return e820__vpmem_signle_numa_exclusive();

	/* NUMA enabled, equalize os memory to each NUMA */
	if (e820__vpmem_mult_numa_equalize() < 0)
		return e820__vpmem_signle_numa_exclusive();

	return e820__vpmem_update_table();
}

void __init e820__rollback_vpmem_to_ram(void)
{
	int i;

	for (i = 0; i < e820_table_vpmem->nr_entries; i++) {
		struct vpmem_early_entry *entry = &e820_table_vpmem->entries[i];

		if (!entry->size)
			continue;
		e820__range_update(entry->addr, entry->size,
			E820_TYPE_VPMEM, E820_TYPE_RAM);
	}
	vpmem_clear_memory_range();
}

